package cn.lg.soar.system.monitor.service.impl;

import cn.lg.soar.common.algorithm.serial.Snowflake;
import cn.lg.soar.common.data.Soar64;
import cn.lg.soar.common.data.msg.I18n;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.IpUtil;
import cn.lg.soar.common.util.ParameterUtil;
import cn.lg.soar.common.util.current.CurrentProxy;
import cn.lg.soar.common.util.current.CurrentUser;
import cn.lg.soar.common.util.current.ThreadLocalHolder;
import cn.lg.soar.common.util.current.UserContext;
import cn.lg.soar.common.util.token.SoarJWT;
import cn.lg.soar.core.config.cache.template.ICacheTemplate;
import cn.lg.soar.core.propertie.security.AccessTokenProperties;
import cn.lg.soar.core.propertie.security.Mode;
import cn.lg.soar.core.propertie.security.RefreshTokenProperties;
import cn.lg.soar.core.propertie.security.SecurityProperties;
import cn.lg.soar.mvc.util.ServletUtils;
import cn.lg.soar.system.monitor.entity.RefreshToken;
import cn.lg.soar.system.auth.entity.User;
import cn.lg.soar.system.monitor.mapper.TokenInfoMapper;
import cn.lg.soar.system.monitor.service.IRefreshTokenService;
import cn.lg.soar.system.auth.service.ITokenCurrBindService;
import cn.lg.soar.system.auth.service.IUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import eu.bitwalker.useragentutils.DeviceType;
import eu.bitwalker.useragentutils.UserAgent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * refreshToken状态管理(可根据自己的需求修改)
 * @author luguoxiang
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class RefreshTokenServiceImpl extends ServiceImpl<TokenInfoMapper, RefreshToken> implements IRefreshTokenService, CurrentProxy<RefreshTokenServiceImpl> {

    @Autowired
    private IUserService userService;
    @Autowired
    private ITokenCurrBindService tokenCurrBindService;
    @Autowired
    private ICacheTemplate<String, String> cacheTemplate;
    @Autowired
    private RefreshTokenProperties refreshTokenProperties;

    private long accessTokenExpires;
    private long shortRefreshTokenExpires;
    @Autowired
    public void setAccessTokenProperties(AccessTokenProperties properties) {
        this.accessTokenExpires = properties.getExpire().getSeconds() * 1000;
        this.shortRefreshTokenExpires = this.accessTokenExpires + 600_000;
    }

    private Mode mode;
    private Map<Integer, String> disabledStatus;
    @Autowired
    public void setSecurityProperties(SecurityProperties properties) {
        this.mode = properties.getMode();
        this.disabledStatus = properties.getUserStatus();
    }

    @Override
    public String create(Long userId, boolean rememberMe) {
        return create(userId, rememberMe, null);
    }

    @Override
    public String refresh(String refreshToken) {
        Object[] payload = SoarJWT.parsePayload(refreshToken, Object[].class);
        // 验证
        RefreshToken token = verify(refreshToken);
        // 生成新的token
        return create(token.getUserId(), Boolean.TRUE.equals(payload[2]), token.getId());
    }

    @Override
    public void clear() {
        CurrentUser currentUser = (CurrentUser) UserContext.getUser();
        AssertUtil.isTrue(removeById(currentUser.getTokenId()), "操作失败");
    }

    @Override
    public void clear(Long userId, Collection<DeviceType> deviceTypes) {
        LambdaQueryWrapper<RefreshToken> wrapper = Wrappers.<RefreshToken>lambdaQuery()
                .likeRight(RefreshToken::getId, Soar64.ofLong(userId));
        if (CollectionUtils.isNotEmpty(deviceTypes)) {
            wrapper.in(
                    RefreshToken::getDeviceType,
                    deviceTypes.stream().map(DeviceType::ordinal).collect(Collectors.toSet())
            );
        }
        remove(wrapper);
    }

    @Override
    public boolean removeByIds(Collection<?> ids) {
        AssertUtil.notEmpty(ids, "缺少id参数");
        AssertUtil.isTrue(super.removeByIds(ids), "清除token失败");
        return true;
    }

    @Override
    public Set<String> removeByUserId(Serializable userId) {
        List<Object> objects = this.listObjs(Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .eq(RefreshToken::getUserId, userId));
        if (CollectionUtils.isEmpty(objects)) {
            return Collections.emptySet();
        }
        Set<String> ids = objects.stream().map(Object::toString).collect(Collectors.toSet());
        removeByIds(ids);
        return ids;
    }

    @Override
    public Set<String> removeByUserId(Serializable userId, Integer deviceType) {
        List<Object> objects = this.listObjs(Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .eq(RefreshToken::getUserId, userId)
                .eq(RefreshToken::getDeviceType, deviceType)
        );
        Set<String> ids = objects.stream().map(Object::toString).collect(Collectors.toSet());
        this.remove(
                Wrappers.<RefreshToken>lambdaQuery()
                        .in(RefreshToken::getId, ids)
                        .or()
                        .le(RefreshToken::getExpires, LocalDateTime.now())
        );
        return ids;
    }

    @Override
    public RefreshToken verify(String refreshToken) {
        HttpServletRequest request = ServletUtils.getRequest();
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        DeviceType deviceType = userAgent.getOperatingSystem().getDeviceType();
        // 准备数据
        Object[] payload = SoarJWT.parsePayload(refreshToken, Object[].class);
        boolean rememberMe = Boolean.TRUE.equals(payload[2]);
        long expires;
        if (rememberMe) {
            expires = refreshTokenProperties.getExpireMillis(deviceType);
        } else {
            expires = shortRefreshTokenExpires;
        }

        // 验证用户(为了取加密盐)
        User user = this.userService.getById((String) payload[ 3 ]);
        ParameterUtil.notNull(user, "用户不存在");
        // 判断用户状态
        String error = disabledStatus.get(user.getStatus());
        if (error != null) {
            AssertUtil.throwException(I18n.build(error));
        }

        // 验证token串有效性
        SoarJWT.Result result = new SoarJWT(user.getSecret() + refreshTokenProperties.getSecret(), expires).verify(refreshToken);
        ParameterUtil.isTrue(result.isValid(), "refresh token 无效");
        // 验证token状态
        String tokenId = (String) payload[ 0 ];
        RefreshToken tokenInfo = getById(tokenId);
        ParameterUtil.isTrue(Objects.nonNull(tokenInfo), "refresh token 无效");

        // 验证是否过期
        if (LocalDateTime.now().isAfter(tokenInfo.getExpires())) {
            removeById(tokenId);
            ParameterUtil.throwException(I18n.build("token 过期"));
        }
        // 验证tokenInfo是否属于当前token
        ParameterUtil.isTrue(Objects.equals(Long.parseLong(payload[ 1 ].toString()), tokenInfo.getValue()), "refresh token 无效");
        return tokenInfo;
    }

    @Override
    public List<String> idsByUserId(List<Long> userIds, DeviceType deviceType) {
        LambdaQueryWrapper<RefreshToken> wrapper = Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .in(RefreshToken::getUserId, userIds);
        if (deviceType != null) {
            wrapper.eq(RefreshToken::getDeviceType, deviceType.ordinal());
        }
        return listObjs(wrapper, Object::toString);
    }

    @Override
    public List<String> idsByUserIdAndActive(List<Long> userIds, DeviceType deviceType) {
        LambdaQueryWrapper<RefreshToken> wrapper = Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .in(RefreshToken::getUserId, userIds)
                .gt(RefreshToken::getTokenExpires, LocalDateTime.now());
        if (deviceType != null) {
            wrapper.eq(RefreshToken::getDeviceType, deviceType.ordinal());
        }
        return listObjs(wrapper, Object::toString);
    }

    private String create(Long userId, boolean rememberMe, String tokenId) {
        // 读取用户信息
        User user = this.userService.getById(userId);
        AssertUtil.notNull(user, "用户不存在");
        // 判断用户状态
        String error = disabledStatus.get(user.getStatus());
        if (error != null) {
            AssertUtil.throwException(I18n.build(error));
        }
        // 获取客户端设备类型
        HttpServletRequest request = ServletUtils.getRequest();
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        DeviceType deviceType = userAgent.getOperatingSystem().getDeviceType();
        // 创建tokenValue和tokenId，tokenId和tokenValue是一对，验证时必须匹配才通过
        long tokenValue = Snowflake.nextValue();
        if (tokenId == null) {
            // 根据模式生成tokenId
            switch (this.mode) {
                case only:
                    tokenId = Soar64.ofLong(user.getId());
                    break;
                case device:
                    tokenId = Soar64.ofLong(user.getId()) + ":" + deviceType.ordinal();
                    break;
                case multi:
                    tokenId = Soar64.ofLong(user.getId()) + ":" + deviceType.ordinal() + ":" + Soar64.ofLong(tokenValue);
                    break;
                default: throw new RuntimeException("不支持的模式");
            }
        }

        // 创建刷新token
        long refreshExpires = rememberMe ? refreshTokenProperties.getExpireMillis(deviceType) : shortRefreshTokenExpires;
        Object[] payload = {
                tokenId,
                tokenValue,
                rememberMe,
                user.getId(),
                user.getUsername(),
                user.getUserType(),
                user.getCurrentTenantId()
        };

        String refreshToken = new SoarJWT(user.getSecret() + refreshTokenProperties.getSecret(), refreshExpires).create(payload);
        long millis = System.currentTimeMillis();
        RefreshToken refreshTokenInfo = new RefreshToken();
        refreshTokenInfo.setId(tokenId);
        refreshTokenInfo.setValue(tokenValue);
        refreshTokenInfo.setUserId(user.getId());
        refreshTokenInfo.setDeviceType(deviceType.ordinal());
        refreshTokenInfo.setIp(IpUtil.getRemoteIpInt(request));
        refreshTokenInfo.setExpires(DatetimeUtil.from(millis + refreshExpires));
        refreshTokenInfo.setTokenExpires(DatetimeUtil.from(millis + accessTokenExpires));

        AssertUtil.isTrue(this.saveOrUpdate(refreshTokenInfo), "保存token信息失败");
        // 3.返回信息
        ThreadLocalHolder.setCurrentUser(CurrentUser.valueOf(payload));
        return refreshToken;
    }

    @Override
    @Scheduled(fixedDelay = 610000)
    public void optimize() {
        // 利用缓存（当使用redis时）实现部署多个微服务实例时，没十分钟只会执行一次
        String s = cacheTemplate.get("refresh-token-optimize");
        if (s == null) {
            List<String> ids = listObjs(
                    Wrappers.<RefreshToken>lambdaQuery()
                            .select(RefreshToken::getId)
                            .le(RefreshToken::getExpires, LocalDateTime.now()),
                    Object::toString
            );
            if (!ids.isEmpty()) {
                tokenCurrBindService.removeByIds(ids);
                this.removeByIds(ids);
            }
            cacheTemplate.put("refresh-token-optimize", "1", 600000);
        }
    }
}
